/**
 * Copyright 2011 Brigham Young University
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package edu.byu.nlp.reader;

import static edu.byu.nlp.io.Files2.open;

import java.io.FileNotFoundException;
import java.util.List;

import com.google.common.collect.Lists;

import edu.byu.nlp.data.BlockFeatureMatrixBuilder;
import edu.byu.nlp.data.Datasets;
import edu.byu.nlp.data.Dimension;
import edu.byu.nlp.data.LabeledDataset;
import edu.byu.nlp.data.LabeledDatasetInfo;
import edu.byu.nlp.data.LabeledInstance;

/**
 * Reads in a classification dataset in the SVMLight format. The SVMLight format is a text format in which there is one
 * instance per line with the first value being the label and subsequent values (separated by whitespace) contain
 * colon-separated pairs of feature indices and values. Comments are indicated by a # and needn't occur at the start of
 * the line. Ommission of a feature index constitutes an implicit zero value and ommission of a value constitutes an
 * implicit value of 1.0. An example is as follow:
 * 
 * {@code
 *      # comment here
 * 		0 0:1.0 5:2.0 7:-1.0
 *      1 3:1 4:1 6 # the value of this last feature is implied to be 1.0
 * }
 * 
 * If a DatasetInfo object is not passed to the read function, then this object scans the file to determine the
 * number of labels, feature indices, and instances and performs a second pass to read the data.
 * 
 * @author rah67
 *
 */
public class SVMLightReader {

	private static interface DataProcessor {
		void processLabel(int label);
		void processFeatureValuePair(int featureIndex, double value);
		void nextInstance();
	}
	
	private static class DatasetInfoProcessor implements DataProcessor {
		private int maxFeatureIndex = -1;
		private int maxLabel = -1;
		private int numInstances = 0;
		
		@Override
		public void processFeatureValuePair(int featureIndex, double value) {
			if (featureIndex > maxFeatureIndex) {
				maxFeatureIndex = featureIndex;
			}
		}

		@Override
		public void processLabel(int label) {
			if (label > maxLabel) {
				maxLabel = label;
			}
		}

		@Override
		public void nextInstance() {
			++numInstances;
		}
		
		public LabeledDatasetInfo getInfo() {
			return new LabeledDatasetInfo(numInstances, maxFeatureIndex + 1, maxLabel + 1);
		}
	}
	
	private static class DatasetBuilderProcessor implements DataProcessor {
		private final LabeledDatasetInfo info;
		private final List<LabeledInstance> data;
		private final BlockFeatureMatrixBuilder builder;
		private int label;
		private int numInstances;
		
		public DatasetBuilderProcessor(LabeledDatasetInfo info) {
			this.info = info;
			this.data = Lists.newArrayListWithCapacity(info.getNumInstances());
			Dimension dimension = new Dimension(info.getNumLabels(), info.getNumFeatures());
			this.builder = new BlockFeatureMatrixBuilder(dimension, true);
			this.numInstances = 0;
		}
		
		@Override
		public void processLabel(int label) {
			this.label = label;
		}

		@Override
		public void processFeatureValuePair(int featureIndex, double value) {
			builder.addFeatureValuePair(featureIndex, value);
		}

		@Override
		public void nextInstance() {
			LabeledInstance instance = new LabeledInstance(builder.buildFeatureMatrix(), label);
			data.add(instance);
			builder.clear();
			++numInstances;
		}

		public LabeledDataset getDataset() throws InvalidFormatException {
			if (numInstances != info.getNumInstances()) {
				throw new InvalidFormatException(
						String.format("Counted %d instances, but expecting %d; check provided info DatasetInfo",
								numInstances,
								info.getNumInstances()));
			}
			return Datasets.newLabeledDataset(data, info);
		}
	}
	
	public LabeledDataset read(String filename) throws FileNotFoundException, InvalidFormatException {
		DatasetInfoProcessor processor = new DatasetInfoProcessor();
		// We prefer two passes in order that immutability of the Dimension object inside of the FeatureMatrix be
		// preserved for efficiency reasons. The hope is that the LabeledDatasetInfo is usually kept around by default
		// and even automatically created to avoid this double pass.
		read(filename, processor);
		return read(filename, processor.getInfo());
	}

	public LabeledDataset read(String filename, LabeledDatasetInfo info) throws FileNotFoundException, InvalidFormatException {
		DatasetBuilderProcessor processor = new DatasetBuilderProcessor(info);
		read(filename, processor);
		return processor.getDataset();
	}
	
	private void read(String filename, DataProcessor processor) throws FileNotFoundException, InvalidFormatException {
		int lineNumber = 1;
		for (String line : open(filename)) {
			// Strip comments
			line = line.replaceFirst("#.*", "");
			
			// Split into components
			String[] columns = line.split("\\s+");
			
			// Allow blank lines (and skip them)
			if (columns.length == 0) {
				continue;
			}
			
			processLabel(processor, lineNumber, line, columns);
			processFeatureValuePairs(processor, lineNumber, line, columns);
			
			++lineNumber;
		} // for each line
	}

	private void processLabel(DataProcessor processor, int lineNumber, String line, String[] columns) throws InvalidFormatException {
		int label = 0;
		try {
			label = Integer.valueOf(columns[0]);
		} catch (NumberFormatException e) {
			throw new InvalidFormatException(String.format(
					"On line %d: the label (first value on line) is not an integer.\nSource line = %s",
					lineNumber, line));
		}
		if (label < 0) {
			throw new InvalidFormatException(String.format(
					"On line %d: the label (%d) is not a valid label (must be non-negative). \nSource line = %s",
					lineNumber, label, line));
		}
		processor.processLabel(label);
	}

	private void processFeatureValuePairs(DataProcessor processor, int lineNumber, String line, String[] columns)
			throws InvalidFormatException {
		for(int col = 1; col < columns.length; col++) {
			String featureValuePair[] = columns[col].split(":");
			if (featureValuePair.length > 2) {
				throw new InvalidFormatException(String.format(
						"On line %d: feature/value pair at index %d contains too many colons.\nSource line = %s",
						lineNumber, col - 1, line));
			}
			
			int featureIndex = 0;
			double value = 0.0;

			try {
				featureIndex = Integer.valueOf(featureValuePair[0]);
			} catch (NumberFormatException e) {
				throw new InvalidFormatException(String.format(
						"On line %d: the feature at index %d is not a valid integer.\nSource line = %s",
						lineNumber, col - 1, line));
			}
			if (featureIndex < 0) {
				throw new InvalidFormatException(String.format(
						"On line %d: the feature at index %d (%d) is not a valid feature index (must be " +
						"non-negative). \nSource line = %s",
						lineNumber, featureIndex, col - 1, line));
			}

			if (featureValuePair.length == 1) {
				// Allow binary data by omitting value
				value = 1.0;
			} else {
				try {
					value = Double.valueOf(featureValuePair[1]);
				} catch (NumberFormatException e) {
					throw new InvalidFormatException(String.format(
							"On line %d: the value at index %d is not a valid double.\nSource line = %s",
							lineNumber, col - 1, line));
				}
			}
			processor.processFeatureValuePair(featureIndex, value);
		}
	}
}
